cover

长安链 Chainmaker 体验记录

本文主要记录了在体验长安链 Chainmaker 过程中遇到的一些问题。如:单机部署管理后台和区块链浏览器;修复 InsertBlockAndTx 卡死;Node.js SDK 不支持国密。记录了针对这些问题的分析以及一些可以尝试的解决思路。

2023-09-11

本次体验长安链 Chainmaker 的过程中主要涉及到了以下几个部分的内容:

  1. 部署管理后台区块链浏览器
  2. 使用管理后台部署区块链
  3. 使用管理后台部署智能合约
  4. 使用管理后台调用智能合约
  5. 使用区块链浏览器查看 Transaction
  6. 使用 sdk 调用智能合约

使用的长安链版本为 v2.3.0,所有操作均按照官方文档中的步骤进行。在体验过程中,问题主要出现在第一、第三和第六上,具体的问题会在后面说明。

后续的内容分成主要分成三个部分:第一部分介绍体验过程中遇到的三个问题;第二部分提供单机部署管理后台和区块链浏览器的方法;第三部分简单介绍了如何实现一个简单的 go-sdk API 服务。

相关代码已上传 Github

遇到的问题

按照官方文档的操作步骤基本上都能够达到预期的目标,但是在体验过程中还是遇到了一下无法在文档及问题列表中找到解决方案的问题,后续内容主要是对这些问题进行介绍。

问题一、单机部署管理后台和区块链浏览器

原本计划是直接使用长安链官方提供管理后台和区块链浏览器的 docker-compose 启动服务,并在宿主机上使用 Nginx 通过子域名反向代理到对应的服务。但是部署完成之后发现两个子域名都只能访问到管理后台应用。

通过查看 management-webexplorer-web 的 Dockerfile 后发现,这两个 web 应用镜像在容器内部已经使用了 Nginx 作为 web 应用服务器,并且 management-web 的 docker-compose 的中的端口映射是 80:80,所以在服务启动之后外部的访问 80 端口时实际上是访问到的是容器内的部的 Nginx 服务。

为了能够使用宿主机的 Ningx 代理管理后台和区块链浏览器,需要修改管理后台服务的端口映射,因此将官方提供的 managementexplorer 的 docker-compose 文件进行合并,并将 cm_mgmt_server 的端口转发设置为 9995:80

具体实现可以参见后文

问题二、管理后台无法订阅部署了智能合约的区块链

使用管理后台生成部署材料并进行部署,并在管理后台订阅了区块链网络。但在提交部署智能合约的时候出现了管理后台服务长时间无法访问,然后自动重启的问题。

查看日志后发现管理后台后端在调用插入 InsertBlockAndTx 的时候卡住了。通过在本地进行调试后发现,当管理后台后端向数据库中插入 Transaction 的时候会卡死。通过修改源码(src/sync/resolver.go)进行调试,发现如果使用了 transaction.ContractParams 字段,则后端应用就会直接卡死。似乎是在 json.unmarshal 的时候出现了问题,但由于对 go 语言并不熟悉,在这里直接用最简单的的方法 - 将 Transaction.ContractParams 相关的代码直接注释掉来回避这个问题,具体内容见后文

问题三、Node.js 的 SDK 不支持国密 SSL

尝试使用长安链提供的 Node.js SDK 调用智能合约,经测试后发现如果使用国密证书进行调用会提示证书不支持(unsupported)。查询后发现目前只有 go-sdk 和 java-sdk 实现了国密 SSL

为了使用国密所以选择使用 go-sdk 实现一个用于调用智能合约的 API 服务

部署管理后台和区块链浏览器

服务器运行环境:

  1. Debian@12;
  2. 安装了 docker 及 docker-compose;

ssh 到服务器中并初始化工程目录:

mkdir -p /src/chainmaker
mkdir -p /data/chainmaker/{management,explorer}

初始化项目

在本地工作目录中按以下的脚本初始化一个工程项目:

# 创建项目目录并初始化
mkdir chainmaker && cd chainmaker && git init
# 以 v2.3.0 为例
# 将 management-backend 作为 submodule 以便避免"问题二"
git submodule add https://git.chainmaker.org.cn/chainmaker/management-backend.git management-backend
pushd management-backend
  git checkout tags/$VERSION -b $VERSION
popd
# 新建两个目录用于保存数据库和后端配置
mkdir {management_configs,explorer_configs}

避免"问题二"

注释掉 management-backend/src/sync/resolver.go 中第 124 行到 129 行的内容以避免上问中提到的问题二。

修改之后的问题是在管理后台的区块链浏览器中无法查看调用合约时的参数,但如果部署了区块链浏览器其实并不是一个很大的问题。同时在调试中发现,只有部署或升级智能合约时的 transaction.ContractParams 会导致应用卡死,因此也可以添加条件语句实现针对性的忽略。

更新默认配置

management_configs 中添加 .env 文件,用于配置管理后台数据库:

MYSQL_ROOT_PASSWORD=<管理后台 Mysql Root Password>
MYSQL_USER=chainmaker
MYSQL_PASSWORD=<chainmaker_mgmt 密码>
MYSQL_DATABASE=chainmaker_mgmt
MYSQL_TCP_PORT=3306

在同一个文件夹下添加 config.yml 文件供管理后台后端服务使用:

web:
  address:  0.0.0.0
  port:     9999
  cross_domain:     true
  session_age: 86400
  captcha:
  height: 80
  width: 200
  noise_count: 5
  length: 4
  # 错误提示语言,取值:0 - 英文,1 - 中文
  errmsg_lang: 1
  # 加载链信息间隔时间,单位:秒
  load_period_seconds : 60
  # 默认 admin 用户密码,和重置用户密码
  password: <管理后台密码>
  # 日志 agent 的端口
  agent_port: 22301
  # 上报日志链接
  report_url: https://bugreport.chainmaker.org.cn/v1/reportLogs

db:
  host: cm_db
  port: 3306
  database: chainmaker_mgmt
  user:   chainmaker
  passwd: <chainmaker_mgmt 密码>

对区块链浏览器的配置同理,在 explorer_configs 中添加 .envconfig.yml 文件:

MYSQL_ROOT_PASSWORD=<区块链浏览器 Mysql Root Password>
MYSQL_USER=chainmaker
MYSQL_PASSWORD=<chainmaker_explorer 密码>
MYSQL_DATABASE=chainmaker_explorer
MYSQL_TCP_PORT=3306
web:
  address: 0.0.0.0
  port: 9997
  cross_domain: true
  node:
  #  链和节点更新时间
  update_time: 30
  #  节点断开连接时间和新增链时间
  sync_time: 30
  chain:
    show_config: true

db:
  host: cm_explorer_db
  port: 3307
  database: chainmaker_explorer
  user: chainmaker
  passwd: <chainmaker_explorer 密码>

使用 docker-compose 运行服务

添加 docker-compose.yml 文件,并填写如下内容:

version: "3.9"

services:
  cm_db:
    container_name: "cm_db"
    image: mysql:5.7
    volumes:
      - /data/chainmaker/mgmt:/var/lib/mysql
    restart: always
    env_file:
      - management_configs/.env
    command:
      [
        "mysqld",
        "--character-set-server=utf8mb4",
        "--collation-server=utf8mb4_unicode_ci",
        "--max_allowed_packet=200M",
      ]

  cm_mgmt_server:
    container_name: "cm_mgmt_server"
    depends_on:
      - cm_db
    build:
      context: management-backend
      dockerfile: Dockerfile
    image: management-backend:v2.3.0
    volumes:
      - /src/chainmaker/management_configs:/chainmaker-management/configs
    ports:
      - "9999:9999"
    restart: always

  cm_mgmt_web:
    container_name: "cm_mgmt_web"
    depends_on:
      - cm_mgmt_server
    image: chainmakerofficial/management-web:v2.3.0
    ports:
      - "9995:80"
    restart: always

  cm_explorer_db:
    container_name: "cm_explorer_db"
    image: mysql:5.7
    volumes:
      - /data/chainmaker/explorer:/var/lib/mysql
    restart: always
    env_file:
      - explorer_configs/.env
    command:
      [
        "mysqld",
        "--character-set-server=utf8mb4",
        "--collation-server=utf8mb4_unicode_ci",
        "--max_allowed_packet=200M",
      ]

  cm_explorer_server:
    container_name: "cm_explorer_server"
    image: chainmakerofficial/explorer-backend:v2.3.0
    volumes:
      - /src/chainmaker/explorer_configs:/chainmaker-explorer-backend/configs
    depends_on:
      - cm_explorer_db
    ports:
      - "9997:9997"
    environment:
      show_config: true
    restart: always
      
  cm_explorer_web:
    container_name: "cm_explorer_web"
    depends_on:
      - cm_explorer_server
    image: chainmakerofficial/explorer-web:v2.3.0
    ports:
      - "9996:8080"
    restart: always

使用 Nginx 代理前端应用

使用以下的 nginx 配置文件分别代理管理后台和区块链浏览器:

server {
  listen 80;
  server_name management.chainmaker-example.com;

  location / {
    proxy_pass http://127.0.0.1:9995;
  }

  location /chainmaker {
    proxy_read_timeout 300;
    proxy_pass http://127.0.0.1:9999;
    client_max_body_size 0;
  }
}

server {
  listen 80;
  server_name explorer.chainmaker-example.com;

  location / {
    proxy_pass http://127.0.0.1:9996;
  }

  location /chainmaker/ {
    proxy_read_timeout 300;
    proxy_pass http://127.0.0.1:9997/chainmaker;
  }

  location /signatures/ {
    proxy_read_timeout 300;
    proxy_pass http://127.0.0.1:9996/;
  }
}

添加部署脚本

添加运行服务的脚本:


cd /src/chainmaker

docker compose down && docker compose up -d --build

ln -sf /src/chainmaker/chainmaker.conf /etc/nginx/sites-enabled/chainmaker.conf

nginx -t && nginx -s reload

添加部署脚本:

REMOTE=$1
# 初始化文件夹
ssh $REMOTE -t "mkdir -p /src/chainmaker && /data/chainmaker/{management, explorer}"
rsync -av . $REMOTE:/src/chainmaker \
  --exclude=.DS_Store \
  --exclude=.git \
  --exclude=build \
  --exclude=node_modules
ssh $REMOTE -t < .scripts/start.sh

部署:

bash .scripts/deploy.sh <user@host>

实现一个简单的 go-sdk API 服务

由于上文提到的"问题三",因此需要使用 go-sdk 实现一个用于调用智能合约的 API 服务。

首先初始化一个 go 项目,并添加依赖:

go get -u chainmaker.org/chainmaker/sdk-go/v2@v2.3.0
go get -u github.com/gin-gonic/gin

main.go 中添加以下代码:

package main

import (
	"fmt"
	"os"

	"chainmaker.org/chainmaker/pb-go/v2/common"
	sdk "chainmaker.org/chainmaker/sdk-go/v2"
	"github.com/gin-gonic/gin"
)

const CONFIG_PATH = "/configs/sdk_config.yml"

func InitSdkClient() *sdk.ChainClient {
	client, err := sdk.NewChainClient(sdk.WithConfPath(CONFIG_PATH))

	if err != nil {
		fmt.Printf("Initialize sdk failed: %s", err)
		os.Exit(1)
		return nil
	}

	fmt.Printf("Client initialized\n")
	return client
}

func CallUserContract(
	client *sdk.ChainClient,
	action string,
	contractName string,
	method string,
	params map[string]string,
) (int, map[string]string) {

	kv_pairs := buildParamPairs(params)
	var resp *common.TxResponse
	var err error
	if action == "invoke" {
		resp, err = client.InvokeContract(
			contractName,
			method,
			"",
			kv_pairs,
			-1,
			true, // sync
		)
	} else {
		resp, err = client.QueryContract(
			contractName,
			method,
			kv_pairs,
			-1,
		)
	}

	if err != nil {
		fmt.Printf("[ERROR] Invoke contract failed: %s", err.Error())
		return 502, map[string]string{
			"message": err.Error(),
		}
	}

	if resp.Code != common.TxStatusCode_SUCCESS {
		return 400, map[string]string{
			"message": resp.ContractResult.Message,
		}
	}

	return 200, map[string]string{
		"message": resp.Message,
		"data":    string(resp.ContractResult.Result),
	}
}

func buildParamPairs(params map[string]string) []*common.KeyValuePair {
	var kv_pairs []*common.KeyValuePair
	for k := range params {
		kv_pairs = append(kv_pairs, &common.KeyValuePair{
			Key:   k,
			Value: []byte(params[k]),
		})
	}
	return kv_pairs
}

type CallContractParamsDto struct {
	Action       string            `json:action`
	ContractName string            `json:contractName`
	Method       string            `json:method`
	Params       map[string]string `json:params`
}

func StartApi(client *sdk.ChainClient) {

	router := gin.Default()

	router.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "Hello, Gin!"})
	})

	router.POST("/contract", func(c *gin.Context) {
		var dto CallContractParamsDto
		if err := c.BindJSON(&dto); err != nil {
			c.JSON(400, gin.H{"message": "Invalid Body"})
			return
		}
		code, data := CallUserContract(client, dto.Action, dto.ContractName, dto.Method, dto.Params)
		c.JSON(code, data)
	})

	fmt.Println("Listening on port :8080")
	router.Run(":8080")
}

func main() {
	client := InitSdkClient()
	StartApi(client)
}

创建一个 Dockerfile 文件用于部署 sdk 服务。

FROM golang:1.21-bookworm as build
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o /app/bin/sdk.bin

FROM debian:bookworm
EXPOSE 8080
WORKDIR /app
COPY --from=build /app/bin /app
ENTRYPOINT ["/app/sdk.bin", "/configs/sdk_config.yml"]

编译镜像:

docker build . -t chainmaker-sdk:v2.3.0

通过管理后台中的区块链管理/区块链概览/下载链配置下载区块链配置文件。假设将配置文件解压到 /root/configs 中,使用编辑器将 /root/configs/sdk\_config.yml 文件中的 ./crypto-config 全部替换为 /configs/crypto-config,然后使用下面的脚本启动容器:

docker run \
  -p 8080:8080 \
  -v /root/configs:/configs \
  -d chainmaker-sdk chainmaker-sdk:v2.3.0

接下来尝试使用 sdk 接口调用智能合约:

curl -X "POST" "http://127.0.0.1:8080/contract" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "action": "invoke",
  "method": "increase",
  "contractName": "counter"
}'
curl -X "POST" "http://127.0.0.1:8080/contract" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{
  "action": "query",
  "method": "query",
  "contractName": "counter"
}'

如果得到的响应为:

{"message":"send QUERY_CONTRACT failed, all client connections are busy"}

则可能是 sdk_config.yml 中的 nodes.tls_host_name 的配置问题。如果使用的是自签发的证书,则确认 nodes.node_addrnodes.tls_host_name 是否正确。如果使用的是管理后台生成的证书,则可以尝试将 nodes.tls_host_name 修改为 localhost 后再重试。